Skip to content

Clean-up classes nested in functions#21478

Open
ilevkivskyi wants to merge 6 commits into
python:masterfrom
ilevkivskyi:refactor-nested-classes
Open

Clean-up classes nested in functions#21478
ilevkivskyi wants to merge 6 commits into
python:masterfrom
ilevkivskyi:refactor-nested-classes

Conversation

@ilevkivskyi
Copy link
Copy Markdown
Member

@ilevkivskyi ilevkivskyi commented May 13, 2026

Fixes #6422
Fixes #13024

TBH the current situation is embarrassing. I decided to finally clean this all up and unify various cases. Now we have same simple rules used by all classes, both regular (first three) and magic:

  • Use the name mangling for everything inside a top-level function.
  • Do not mangle defn.name only defn.fullname.
  • Always store classes nested in functions in global symbol table (using enclosing class was only adding unnecessary complications)
  • In cases of name mismatch (like One = NamedTuple("Other", ...) etc) always use the var_name for all purposes.
  • In cases of inline base classes use different mangling mechanism (since those may be nested in functions as well).

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@ilevkivskyi
Copy link
Copy Markdown
Member Author

It looks like this also fixes #13024 I am going to add a test case.

@ilevkivskyi
Copy link
Copy Markdown
Member Author

Btw mypy_primer is also nice, users don't need to see those @252 suffixes in error messages, unless needed (i.e. unless format_type_distinctly will decide to use full names).

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for cleaning this up! Looks good overall, but I have a few concerns about cases where we no longer include a line number.

Comment thread test-data/unit/check-classes.test Outdated
def f() -> None:
class X:
...
x # E: Name "x" is not defined
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lower case vs upper case X. If intended, maybe add comment, since they look pretty similar.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am going to rename x to undefined.

Comment thread test-data/unit/semanal-namedtuple.test Outdated
A
TupleType(
tuple[Any, fallback=__main__.N@2])
tuple[Any, fallback=__main__.namedtuple@N])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we no longer have a line number, what happens if we have multiple namedtuple('N', ...') base classes in a single file?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to say that this can't happen because there can't be two classes with the same name, but apparently we use the string name (i.e. N, not A) in such cases. I think a proper solution would be to use something like A@base1, etc.

To be clear I don't want to use line number for this disambiguation as well, since we can get weird things like Foo@7@7 if we need to apply both (e.g. namedtuple base class for a class nested in function).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a proper solution would be to use something like A@base1, etc.

Yes, this allowed to delete another couple dozen lines of tricky logic. I also added couple incremental tests to make sure all is good in such cases.

Comment thread mypy/semanal.py Outdated
# The second one is needed to properly serialize any classes nested in functions.
# TODO: make sure the daemon works well with these rules.
if self.is_nested_within_func_scope():
class_def.fullname = f"{self.cur_mod_id}.{name}@{line}"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name generation logic is duplicated in a few places -- maybe move it to a helper function?

Comment thread mypy/semanal_namedtuple.py Outdated
name += "@" + str(call.line)
# * This is a local (function or method level) named tuple, this case is
# handled by basic_new_typeinfo().
name = "namedtuple@" + name
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in my other comment, I wonder if preserving the line number would be helpful here.

Comment thread mypy/semanal_typeddict.py Outdated

if var_name is None:
# Give it unique name in case a TypedDict() call is used as a base class.
name = "TypedDict@" + name
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks similar to namedtuple case -- would the line number be helpful here, in case there are multiple base classes in the same file?

@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

zulip (https://github.com/zulip/zulip)
- zerver/lib/user_topics.py:265: error: Argument 1 to "map" has incompatible type "Callable[[RecipientTopicDict@252], Any]"; expected "Callable[[dict[str, Any]], Any]"  [arg-type]
+ zerver/lib/user_topics.py:265: error: Argument 1 to "map" has incompatible type "Callable[[RecipientTopicDict], Any]"; expected "Callable[[dict[str, Any]], Any]"  [arg-type]
- zerver/lib/user_topics.py:303: error: Argument 1 to "map" has incompatible type "Callable[[RecipientTopicDict@252], Any]"; expected "Callable[[dict[str, Any]], Any]"  [arg-type]
+ zerver/lib/user_topics.py:303: error: Argument 1 to "map" has incompatible type "Callable[[RecipientTopicDict], Any]"; expected "Callable[[dict[str, Any]], Any]"  [arg-type]

[file lib.py]
from typing import NamedTuple
class A(NamedTuple("N", [('x', int)])): pass
class A(NamedTuple("N", [('x', str)])): pass
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the second one is like class B(NamedTuple("N", ...)), i.e. only the base class string literal name is common?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said, string name is always ignored now. But I can add a test if you want.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine.

Copy link
Copy Markdown
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates!

[file lib.py]
from typing import NamedTuple
class A(NamedTuple("N", [('x', int)])): pass
class A(NamedTuple("N", [('x', str)])): pass
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use of undefined variable causes unrelated local class to leak scope New semantic analyzer: Clean-up logic for classes in functions

2 participants